package org.otarriinae.security.authenticate.filter;


import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import lombok.extern.slf4j.Slf4j;
import org.orarriinae.common.base.exception.ServiceException;
import org.orarriinae.common.web.result.ResponseBean;
import org.otarriinae.kaptcha.service.KaptchaService;
import org.otarriinae.security.authenticate.modle.SecurityLogin;
import org.otarriinae.security.base.constant.StringsConstant;
import org.otarriinae.security.base.dto.UserDto;
import org.otarriinae.security.base.model.TokenUserDetails;
import org.otarriinae.security.base.service.TokenManager;
import org.otarriinae.security.log.login.LoginFailEvent;
import org.otarriinae.security.log.login.LoginSuccessEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.core.GenericTypeResolver;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.stream.Collectors;

/**
 * @author xj
 * @since 1.0
 */
@Slf4j
public class TokenLoginFilter extends AbstractAuthenticationProcessingFilter{

    private final TokenManager tokenManager;
    private final AuthenticationManager authenticationManager;
    private final KaptchaService kaptchaService;
    private final ApplicationContext applicationContext;

    private static final String POST = "POST";

    public TokenLoginFilter(TokenManager tokenManager, AuthenticationManager authenticationManager, KaptchaService kaptchaService, ApplicationContext applicationContext) {
        super(new AntPathRequestMatcher("/login", "POST"));
        this.tokenManager = tokenManager;
        this.authenticationManager = authenticationManager;
        this.kaptchaService = kaptchaService;
        this.applicationContext = applicationContext;
    }

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = false;

    private ObjectMapper objectMapper = new ObjectMapper();

    protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
        TypeFactory typeFactory = this.objectMapper.getTypeFactory();
        return typeFactory.constructType(GenericTypeResolver.resolveType(type, contextClass));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !POST.equals(request.getMethod())) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        SecurityLogin securityLogin = getSecurityLogin(request);
        // 验证码校验
        kaptchaService.validCode(securityLogin.getKey(),
                securityLogin.getVerifyCode());

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                securityLogin.getUsername(), securityLogin.getPassword());

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.authenticationManager.authenticate(authRequest);
    }

    private SecurityLogin getSecurityLogin(HttpServletRequest request) {
        JavaType javaType = this.getJavaType(SecurityLogin.class, (Class) null);
        SecurityLogin securityLogin = null;
        try {
            securityLogin = this.objectMapper.readValue(request.getInputStream(), javaType);
        } catch (IOException e) {
            throw new ServiceException("登录请求体异常");
        }
        return securityLogin;
    }

    /**
     * Enables subclasses to override the composition of the password, such as by
     * including additional values and a separator.
     * <p>
     * This might be used for example if a postcode/zipcode was required in addition to
     * the password. A delimiter such as a pipe (|) should be used to separate the
     * password and extended value(s). The <code>AuthenticationDao</code> will need to
     * generate the expected password in a corresponding manner.
     * </p>
     *
     * @param request so that request attributes can be retrieved
     * @return the password that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(passwordParameter);
    }

    /**
     * Enables subclasses to override the composition of the username, such as by
     * including additional values and a separator.
     *
     * @param request so that request attributes can be retrieved
     * @return the username that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(usernameParameter);
    }

    /**
     * Provided so that subclasses may configure what is put into the authentication
     * request's details property.
     *
     * @param request     that an authentication request is being created for
     * @param authRequest the authentication request object that should have its details
     *                    set
     */
    protected void setDetails(HttpServletRequest request,
                              UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    /**
     * Sets the parameter name which will be used to obtain the username from the login
     * request.
     *
     * @param usernameParameter the parameter name. Defaults to "username".
     */
    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    /**
     * Sets the parameter name which will be used to obtain the password from the login
     * request..
     *
     * @param passwordParameter the parameter name. Defaults to "password".
     */
    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    /**
     * Defines whether only HTTP POST requests will be allowed by this org.otarriinae.security.authenticate.filter. If set to
     * true, and an authentication request is received which is not a POST request, an
     * exception will be raised immediately and authentication will not be attempted. The
     * <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
     * authentication.
     * <p>
     * Defaults to <tt>true</tt> but may be overridden by subclasses.
     */
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return usernameParameter;
    }

    public final String getPasswordParameter() {
        return passwordParameter;
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        TokenUserDetails user = (TokenUserDetails) auth.getPrincipal();

        String token = tokenManager.createToken(user);

        UserDto userDto = new UserDto();
        user.setUserId(user.getUserId());
        userDto.setUsername(user.getUsername());
        userDto.setToken(token);
        userDto.setCategoryMap(user.getCategoryMap());
        userDto.setAuthorities(user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
        ResponseBean responseBean = ResponseBean.success(userDto);
        // 发送登录成功消息
        applicationContext.publishEvent(new LoginSuccessEvent(this, user.getUserId()));
        res.setStatus(HttpStatus.OK.value());
        res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        JSON.writeJSONString(res.getWriter(), responseBean);

    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
        response.setStatus(HttpStatus.OK.value());

        ResponseBean responseBean = new ResponseBean();
        if (e instanceof BadCredentialsException) {
            log.warn("登录失败:{}", e);
        } else if (e instanceof UsernameNotFoundException) {
            log.warn("登录失败:{}", e);
        } else if (e instanceof AuthenticationCredentialsNotFoundException) {
            log.warn("登录失败:{}", e);
        } else if (e instanceof ProviderNotFoundException) {
            log.warn("登录失败:{}", e);
        } else {
            log.warn("登录失败:{}", e);
        }
        // 发送登录失败消息
        applicationContext.publishEvent(new LoginFailEvent(this));
        responseBean.setErrorCode(StringsConstant.ERROR_CODE);
        responseBean.setMessage("登录失败");
        responseBean.setState(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        JSON.writeJSONString(response.getWriter(), responseBean);

    }

}
